package com.swingson;

import com.swingson.controller.Controller;
import com.swingson.event.ValueEvent;
import com.swingson.other.ConditionAndAction;
import com.swingson.other.SwingularComponentWrapper;
import java.awt.Component;
import java.lang.reflect.Field;
import java.util.*;
import javax.swing.ButtonGroup;
import javax.swing.JComponent;
import org.mvel2.MVEL;
import org.mvel2.integration.VariableResolver;
import org.mvel2.integration.impl.MapVariableResolverFactory;

/**
 * @author sad
 */
public class SwingsonGui {

    public class DefinedComponent {

        private final Field field;
        private final JComponent component;

        public DefinedComponent(Field field, JComponent value) {
            this.field = field;
            this.component = value;
        }

        public JComponent getComponent() {
            return component;
        }

        public Field getField() {
            return field;
        }
    }

    private JComponent topComponent;
    private Object controllerComponent;
    private final List<ValueEvent<Throwable>> exceptionHandlerList = new ArrayList<>();
    private final Map<String, DefinedComponent> definedComponents = new HashMap<>();
    private final Map<String, ButtonGroup> radioButtonGroupsMap = new HashMap<>();

    private final Map<Component, SwingularComponentWrapper> componentsToSwingularMap = new HashMap<>();
    private final Map<String, List<SwingularComponentWrapper>> inputVariableToSwingularComponentMap = new HashMap<>();

    private final List<Controller> controllers = new ArrayList<>();
    //list of variables that was touched while executing expression
    private final TreeSet<String> touchedVars = new TreeSet<>();
    //for resolving circular dependency
    private final TreeSet<String> processedVars = new TreeSet<>();

    private class MyMapVariableResolverFactory extends MapVariableResolverFactory {
        @Override
        public VariableResolver createVariable(String name, Object value) {
            if (!processedVars.contains(name)) {
                touchedVars.add(name);
            }
            return super.createVariable(name, value); //To change body of generated methods, choose Tools | Templates.
        }

        @Override
        public VariableResolver createVariable(String name, Object value, Class<?> type) {
            if (!processedVars.contains(name)) {
                touchedVars.add(name);
            }
            return super.createVariable(name, value, type); //To change body of generated methods, choose Tools | Templates.
        }

        public void removeVariable(String name) {
            variables.remove(name);
            variableResolvers.remove(name);
        }
    };

    private final MyMapVariableResolverFactory mvelVariables = new MyMapVariableResolverFactory();

    public void addSwingularComponent(Component component, List<String> inputVars, ConditionAndAction expression) {
        SwingularComponentWrapper wrapper = componentsToSwingularMap.get(component);
        if (wrapper == null) {
            wrapper = new SwingularComponentWrapper(component, expression);
            componentsToSwingularMap.put(component, wrapper);
        } else {
            wrapper.addInput(inputVars);
            wrapper.getCompiledExpressionList().add(expression);
        }

        for (String var : inputVars) {
            List<SwingularComponentWrapper> components = inputVariableToSwingularComponentMap.get(var);
            if (components == null) {
                components = new ArrayList<>();
                inputVariableToSwingularComponentMap.put(var, components);
            }

            components.add(wrapper);
        }
    }

    public List<Controller> getControllers() {
        return controllers;
    }

    public Map<String, ButtonGroup> getRadioButtonGroupsMap() {
        return radioButtonGroupsMap;
    }

    public void deleteComponent(Component component) {
        deleteSwingularComponent(component);
    }

    private void deleteSwingularComponent(Component component) {
        SwingularComponentWrapper comp = componentsToSwingularMap.remove(component);
        if (comp == null) {
            return;
        }

        for (String var : comp.getInputs()) {
            List<SwingularComponentWrapper> components = inputVariableToSwingularComponentMap.get(var);
            components.remove(comp);
        }
    }

    public void addExceptionHandler(ValueEvent<Throwable> exceptionHandler) {
        exceptionHandlerList.add(exceptionHandler);
    }

    public void onException(Throwable t) {
        if (exceptionHandlerList.isEmpty()) {
            if (exceptionHandlerList.isEmpty()) {
                t.printStackTrace();
            }
        }
        try {
            for (ValueEvent e : exceptionHandlerList) {
                e.fire(t);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public SwingsonGui addDefinedComponent(String name, Field field, JComponent value) {
        if (definedComponents.containsKey(name)) {
            System.out.println("Component already contains subcomponents with name " + name);
        }

        DefinedComponent dc = new DefinedComponent(field, value);
        definedComponents.put(name, dc);
        return this;
    }

    public DefinedComponent getDefinedComponent(String name) {
        return definedComponents.get(name);
    }

    public JComponent getTopComponent() {
        return topComponent;
    }

    public void setTopComponent(JComponent topComponent) {
        this.topComponent = topComponent;
    }

    public Object getControllerComponent() {
        return controllerComponent;
    }

    public void setControllerComponent(Object controllerComponent) {
        this.controllerComponent = controllerComponent;
    }

    public Object getVariable(String varName) {
        VariableResolver resolver = mvelVariables.getVariableResolver(varName);
        if (resolver != null) {
            return resolver.getValue();
        } else {
            return null;
        }
    }

    //variables that should be executed only one time and than will be removed
    private final List<String> oneshotVariables = new ArrayList<>();

    public void setVariableSilently(String varName, Object value, boolean oneShot) {
        mvelVariables.createVariable(varName, value);
        if (oneShot) {
            oneshotVariables.add(varName);
        }
    }

    private void removeOneShotVariables() {
        for (String varName : oneshotVariables) {
            mvelVariables.removeVariable(varName);
        }
    }

    public void removeVariableSilently(String name) {
        mvelVariables.removeVariable(name);
    }

    public void setVariable(String varName, Object value) {
        processedVars.clear();
        mvelVariables.createVariable(varName, value);
        touchedVars.clear();

        fireVariableChanged(varName);
        while (!touchedVars.isEmpty()) {
            String touchedVarName = touchedVars.first();
            touchedVars.remove(touchedVarName);
            fireVariableChanged(touchedVarName);
        }

        removeOneShotVariables();
    }

    private void fireVariableChanged(String varName) {
        processedVars.add(varName);
        List<SwingularComponentWrapper> components = inputVariableToSwingularComponentMap.get(varName);
        if (components != null) {
            for (SwingularComponentWrapper comp : components) {
                processSwingularComponentExpression(comp, varName);
            }
        }
    }

    private void processSwingularComponentExpression(SwingularComponentWrapper component, String varName) {
        for (ConditionAndAction caa : component.getCompiledExpressionList()) {
            if (caa.shouldProcess(varName)) {
                runConditionAndActionInternal(caa, component.getComponent());
            }
        }
    }

    private void runConditionAndActionInternal(ConditionAndAction action, Component component) {
        Object result = MVEL.executeExpression(action.getExecutableAccessor(), component, mvelVariables);
        if (action.getEvent() != null) {
            action.getEvent().fire(result);
        }
    }

    public void runConditionAndAction(ConditionAndAction action, Component component) {
        processedVars.clear();
        touchedVars.clear();
        runConditionAndActionInternal(action, component);
        while (!touchedVars.isEmpty()) {
            String touchedVarName = touchedVars.first();
            touchedVars.remove(touchedVarName);
            fireVariableChanged(touchedVarName);
        }

        removeOneShotVariables();
    }
}
